<?php

/** File system related funcs.
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *  @author Jyry Kuukkanen
 *  @copyright Jyry Kuukkanen
 *
 *  $Id: sofiles.php,v 1.12 2005/04/27 11:15:01 jyry Exp $
 */

include_once("sogeneric.php");
include_once("sostring.php");

/* Sorting attribute */
define(SODS_NAME, "n");     /* Entry name */
define(SODS_SIZE, "s");     /* Size */
define(SODS_CREATE, "c");   /* Create time */
define(SODS_MODIFY, "m");   /* Modified time */


/* File name parsing */
define(SOPF_BODY, "body");  /* Actual filename */
define(SOPF_EXT, "ext");    /* File type */


/** Directory class.
 *  Class to read in file names and specified property from specified folder.
 *  Example:
 *  $d = soDir("/tmp", "file", "s");
 *  if ($d->count()) 
 *      echo "Smallest file is ".$d->getName(0)." (".$d->getValue(0)." bytes)";
 *  @var array/array $Files 2d array of file properties and names
 *  @var string $Type Entry type to read in (uses filetype() keywords like
 *      "file", "dir".
 *  @var string $Folder Folder name to read directory from.
 *  @package sofiles
 */
class soDir {

    var $Files;
    var $Type;
    var $Folder;


    /** Constructor
     *  Initialises the object and reads in specified directory listing.
     *  @param string $Folder See read method for more details.
     *  @param string $Type Type of entries to read (null string = all types).
     *      A type can be any PHP's filetype function's keyword.
     *  @param string $SortCol See read() method for more details.
     *  @package sofiles
     */
    function soDir($Folder, $Type = "", $SortCol = "n") {
        $this->Folder = $Folder;
        $this->Type = $Type;
        $this->Files = array();

        $this->read($SortCol);
        return;
    } // soDir


    /** Reads in a directory listing.
     *  @param string $SortCol File property to sort entries by. See SODS_*
     *      for details.
     *  @param string $Folder Name of a folder to read dir entries from.
     *  @return bool True(1) when input params ok, otherwise false(0)
     *  @package sofiles
     */
    function read($SortCol, $Folder = "") {
        if ($Folder != "") $this->Folder = $Folder;
        if (($this->Folder == "") || ($SortCol == "")) return 0; // <-- EXIT

        $dh = opendir($this->Folder);
        if (!$dh) return 0; // <-- EXIT

        switch ($SortCol) {
            case SODS_MODIFY:
                $cols = SODS_MODIFY.SODS_NAME.SODS_SIZE.SODS_CREATE;
                break;
            case SODS_SIZE:
                $cols = SODS_SIZE.SODS_NAME.SODS_MODIFY.SODS_CREATE;
                break;
            case SODS_CREATE:
                $cols = SODS_CREATE.SODS_SIZE.SODS_NAME.SODS_MODIFY;
                break;
            default: // SODS_NAME
                $cols = SODS_NAME.SODS_SIZE.SODS_MODIFY.SODS_CREATE;
        }; // switch

        $ccount = strlen($cols);
        while ($file = @readdir($dh)) {
            $row = array();
            for ($c = 0; $c < $ccount; $c++) {
                switch ($cols[$c]) {
                    case "m":
                        $value = filemtime($this->Folder."/".$file);
                        break;

                    case "s":
                        $value = filesize($this->Folder."/".$file);
                        break;

                    default:
                        $value = $file;
                };
                $row[$cols[$c]] = $value;
            };

            if (($this->Type == "") || 
                (@filetype($this->Folder."/".$file) == $this->Type)) {
                $this->Files[] = $row;
            };
        }; // while
    
        @closedir($dh);

        sort($this->Files);
    	return 1;
    } // read


    /** Number of dir entries found.
     *  @return in Number of files found in current folder
     *  @package sofiles
     */
    function count() {
        return count($this->Files);
    } // count


    /** Check specified entry pos validity.
     *  @param int $Pos Position to check validity of.
     *  @return int Returns true(1) or false(0) whether specified pos is whithin
     *  the range of Files property.
     *  @package sofiles
     */
    function __validPos($Pos) {
        if (($Pos < 0) || ($Pos >= $this->count())) {
            return 0;
        } else {
            return 1;
        };
    } // __validPos


    /** Returns specified file property.
     *  @param int $Pos File entry position to get property of.
     *  @param string $SortCol Property to return.
     *  @return string Returns file property.
     *  @package sofiles
     */
    function getValue($Pos, $SortCol = "n") {
        if (!$this->__validPos($Pos)) return "";

        return $this->Files[$Pos][$SortCol];
    } // getValue

}; // soDir


/** Returns a compressed file as a string.
 *  @param string $FileName Compressed file to read in.
 *  @param SOCM_* Compression method, see SOCM_* define in sogeneric for more
 *  @param bool $SearchPaths When set(1), causes the function to loop through
 *      all include paths in serach for the file.
 *  @return string File content as a string. A null string "" is
 *      returned in case of an error (file not found etc.).
 *  @package sofiles
 */
function soZFileToString($FileName, $Compression, $SearchPaths = 0) {
    if (!file_exists($FileName)) {
        if ($SearchPaths) $FileName = soFindFile($FileName);
        if (!file_exists($FileName)) return NULL;
    }; // if

    $fh = @gzopen($FileName, "r");
    if ($fh) {
        $result = @gzread($fh, filesize($FileName));
        @gzclose($fh);
    } else {
        $result = "";
    };

    return $result;
}; // soZFileToString


/** Returns a file as a string.
 *  Returns specified file as a string.
 *  @param string $FileName File to read in.
 *  @param bool $SearchPaths When set(1), causes the function to loop through
 *      all include paths in serach for the file.
 *  @return string File content as a string. A null string "" is
 *      returned in case of an error (file not found etc.).
 *  @package sofiles
 */
function soFileToString($FileName, $SearchPaths = 0) {
    if (!file_exists($FileName)) {
        if ($SearchPaths) $FileName = soFindFile($FileName);
        if (!file_exists($FileName)) return NULL;
    }; // if

    $fh = @fopen($FileName, "r");
    if ($fh) {
        $result = fread($fh, filesize($FileName));
        @fclose($fh);
    } else {
        $result = "";
    };

    return $result;
}; // soFileToString


/** Returns a file as an array.
 *  Returns specified file as a array. A delimiter string must be specified
 *  too, in order to separate one element from another. Typically this is
 *  a line delimiter "\n" or "\c\n".
 *  @param string $FileName File to read in.
 *  @param string $Delim String to delimit one element from another.
 *  @param bool $SearchPaths When set(1), causes the function to loop through
 *      all include paths in serach for the file.
 *  @return array/string Returns file content as an array. An empty array is
 *      returned in case of an i/o error -OR- if delimeter is a null string.
 *  @package sofiles
 */
function soFileToArray($FileName, $Delim, $SearchPaths = 0) {
    /* quit earlier if delim of file is empty string */

    $result = array();
    if ($Delim == "") return array(); // <-- EXIT

    $file = soFileToString($FileName, $SearchPaths);
    if ($file == "") return array(); // <-- EXIT

    if (soStrPos($file, $Delim)) {
      $result = explode($Delim, $file);
    } else {
      $result = array($file);
    };

    return $result;
}; // soFileToArray


/** Reads in an CSV file to an array.
 *  @param string $FileName File name to read in.
 *  @param string $Delim Delimiter char/str.
 *  @param int $MaxLineLen When not set (0), uses file size as max line
 *      length. If known, what the max line length could not exceed, it
 *      is recomended performance wise to use it.
 *  @param string $Progress Progress symbol/string to print every 4 seconds.
 *      When NULL, print nothing, when set to " " (space), prints current
 *      row no. Any symbol or row no. is always followed by a space.
 *      For example, setting this to "*", would print (echo) a "* " every
 *      4 seconds.
 *  @return array/array/any CSV file as 2-D array.
 */
function soCsvFileToArray($FileName, $Delim = ",", 
                          $MaxLineLen = 0, $Progress = NULL) {
    $result = array();
    $fp = fopen ($FileName, "r");
    $r = 0;
    $printed = 0;
    if (!$MaxLineLen) $MaxLineLen = filesize($FileName);
    while ($line = fgetcsv($fp, $MaxLineLen, $Delim)) {
        $result[] = $line;
        $r++;
        if (!(time() % 4)) {
            if (!$printed) {
                switch ($Progress) {
                    case " ":
                        echo $r." "; flush();
                        break;
                    case NULL:
                        break;
                    default: // any other symbol
                        echo $Progress." "; flush();
                }; // switch
                $printed = 1;
            }; // if
        } else {
            $printed = 0;
        };
    }; // while
    fclose ($fp);
    return $result;
}; // soCsvFileToArray


/** Returns the first ($Desc = 0) or last ($Desc = 1) directory
 *   entry by date
 *   *** NOTE ***!
 *  All programs using this function should be modified to use soDir class
 *  instead. This function is for compatibility reasons only.
 *  --jq
 *  @package sofiles
 */
function soFirstDirEntryByTime($Folder, $Desc) {

    $dir = soDirList($Folder, "", "");

    $d = 0;
    while (($dir[$d]["name"] == ".") || ($dir[$d]["name"] == "..")) {
        $d = $d +1;
    };

    if ($d < count($dir)) {
        $name = $dir[$d]["name"];
        $first = $dir[$d]["mtime"];

        for ($d = 1; $d < count($dir); $d++) {
            $time = $dir[$d]["mtime"];
            if ((substr($dir[$d]["name"], 0, 1) != ".") && ((($Desc) && ($first < $time)) or (!($Desc) && ($first > $time)))) {
                $first = $time;
                $name = $dir[$d]["name"];
            }; // if

        }; // for
    }; // if ($d < count...

    if (substr($name, 0, 1) == '.') $name = "";

    return $name;
}; // soFirstDirEntryByTime


/** Return directory content as an array.
 *  Returns an array of dir entries in $Folder. All entries are sorted
 *  according to value in $Key ("name", "size" or "mtime") from a to z
 *  Example: = 0
 *
 *  *** NOTE ***!
 *  This function is for compatibility reasons only. Any program using this
 *  should be changed to use soDir object instead.
 *  --jq
 *  @package sofiles
 */
function soDirList($Folder, $Key = "", $Az = 0) {
  /*
    $dir is an array that contains the return value of DirList.
    Each of it's row has 3 cols:
    0 "name": Filename
    1 "size": Filesize
    2 "mtime": Modification time    
  */
  $dir = array();	

  $dh = opendir($Folder);
  while ($file = readdir($dh)) {

    $dir[] = array(
                   "name" => $file, 
                   "size" => filesize($Folder . "/" . $file),
                   "mtime" => filemtime($Folder . "/" . $file)
                  );
  }; // while
  closedir($dh);

  sort($dir);

  return $dir;
}; // soDirList


/** Parses body or extension from a filename.
 *  @param string $Filename Filename to parse
 *  @param string $Type "body" or "ext"
 *  @return string Body or extension of the filename
 *  Example:  soParseFilename("one.two.txt", "body") would return "one.two"
 *  @package sofiles
 */
function soParseFilename($Filename, $Type = "body") {
    $dot_pos = soLocateStr($Filename, ".", -1);
    if ($dot_pos == -1) return $Filename; // <-- EXIT

    if ($Type == "ext") {
        $result = substr($Filename, $dot_pos +1);
    } else {
        $result = substr($Filename, 0, $dot_pos);
    };

    return $result;
}; // soParseFileName


/** Returns argv array
 *  Simple function to access argv inside $GLOBALS array.
 *  @return array/any Argv array.
 */
function soGetArgv() {
    $server_vars = $GLOBALS['HTTP_SERVER_VARS'];
    return $server_vars['argv'];
}; // getArgv


/** Looks for a file using search paths define in php.ini
 *  Loops through all paths in include_path and returns the first occurance
 *  of the file.
 *  Example: soFindFile("foo") might return "/some/location/foo"
 *  Example: SoFindFile("dirname/foo") might return "/some/where/dirname/foo"
 *  @param string $FileName Name of the file to be searched
 *  @param string $DirOnly When set, returns only the path name where the
 *      file was found, not full path with file name.
 *  @return string Complete file name with file path or an empty string if
 *      not found anywhere.
 */
function soFindFile($FileName, $DirOnly = 0) {
    $result = "";

    $paths = split('[;:]', ini_get('include_path'));
    $pcount = count($paths);
    $p = -1;
    while ((++$p < $pcount) && ($result == "")) {
        $file = $paths[$p]."/$FileName";
        if (file_exists($file)) $result = $file;
    }; // while

    if ($DirOnly) $result = dirname($result);

    return $result;
}; // soFindFile


/** Joins strings in array eliminating double delimeters.
 *  The purpose of this function is to produce nicer/valid file paths
 *  so that there are no double delimiters like "//" between to folder
 *  names.
 *  For example, soPathJoin(array("/one/", "/two")) would result in "/one/two"
 *  @param array/string $Paths Array of file path string to join.
 *  @param bool $TrailingDelim When set, adds trailing delimiter to the end
 *  of resulting string.
 *  @param string $Delim Delimiter string.
 *  @return string Concatenated file path of all input paths.
 */
function soPathJoin($Paths, $TrailingDelim = 0, $Delim = "/") {
    /* No more than one character in Delim */
    $Delim = substr($Delim, 0, 1);
    
    $pcount = count($Paths);
    $result = $Paths[0];
    for ($p = 1; $p < $pcount; $p++) {
        $res_len = strlen($result);
        if (substr($result, $res_len -1) == $Delim) {
            $result = substr($result, 0, $res_len -1);
        };
        
        $path = $Paths[$p];
        if (substr($path, 0, 1) == $Delim) $path = substr($path, 1);
        $result = soJoinStr($result, $Delim, $path, 1);
    }; // for
    if (($TrailingDelim) && ($result)) $result .= $Delim;
    
    return $result;
}; // soPathJoin


/** Runs a OS command.
 *  For example: soRunCommand('find / -type f, 5') executes and returns
 *  the output of 'find' command and prints out a dot '.' every 5 seconds
 *  For example: soRunCommand('ls', 0) executes 'ls'command and returns
 *  the output without printing out anything.
 *  @param string $Command Command to execute.
 *  @param int $Verbose 0 = quiet, print nothing, -1 = print out all
 *  read from stdout, >0 = interval in seconds to print out a dot '.'.
 *  @return string Command's stdout
 */
function soRunCommand($Command, $Verbose = 0) {
    $ph = popen($Command, "r");
    $result = "";
    $start = time();
    while($stdout = fread($ph, 4096)) {
        if ($Verbose < 0) {
            echo $stdout;
        } elseif (($Verbose > 0) && ($start +$Verbose < time())) {
            echo '.';
            $start = time();
        };
        $result .= $stdout;
    }; // while
    pclose($ph);

    return $result;
}; // soRunCommand


/** Returns free space for a partition/device.
 *  @param string $Device Device name as in fstab
 *  @param string $Type Space type: t=total space, u=used, f or a=available
 *  @param string $Unit Unit (k=kilobytes, m=megs, g=gigs)
 *  @return int Free space on specified device.
 */
function soGetDeviceSpace($Device, $Type = "t", $Unit = "k") {
    $stdout = soExplode(soRunCommand("df"), "\n");
    $result = -1;
    switch ($Type) {
        case "t":
            $pos = 1;
            break;
        case "u":
            $pos = 2;
            break;
        default: // free/avail
            $pos = 3;
            break;
    }; // switch

    $s = count($stdout);
    while (--$s >= 0) {
        $line = soExplode(soReplaceStr($stdout[$s], "  ", " "), " ");
        if ($line[0] == $Device) {
            $result = $line[$pos];
            switch ($Unit) {
                case "m":
                    $result = round($result /1024);
                    break;
                case "g":
                    $result = round($result /(1024 *1024));
                    break;
            }; // switch
            $s = 0;
        }; // if
    }; // while
    
    return $result;
}; // soGetDeviceSpace


/** Returns device name for a mount point.
 *  Example: soGetDeviceName("/boot") could return "/dev/sda1"
 *  @param string $MountPoint
 *  @return string Device name or ampty string when not found.
 */
function soGetDeviceName($MountPoint) {
    $stdout = soExplode(soRunCommand("df"), "\n");
    $result = "";
    $s = count($stdout);
    while (--$s >= 0) {
        $line = soExplode(soReplaceStr($stdout[$s], "  ", " "), " ");
        if ($line[5] == $MountPoint) {
            $result = $line[0];
            $s = 0;
        }; // if
    }; // while

    if ($result) {
        return $result;
    } else {
        return soGetDeviceName(dirname($MountPoint));
    };
}; // soGetDeviceName

/** Returns total size of a folder and its subfolders.
 *  @param string $Folder Folder name
 *  @param string $Unit Returns the size in k=kilobytes, m=megs, g=gigs
 *  @return int Size of the folder.
 */
function soGetFolderSize($Folder, $Unit = "k") {
    $stdout = soExplode(soRunCommand("du -s".$Unit." ".$Folder), "\t");
    return $stdout[0];
}; // soGetFolderSize


/** Deletes files or folders.
 *  Example: delete
 *  @param string $Mask File mask (e.g. "/tmp/*")
 *  @param string $Options Options for "rm", the "-" prefix is not required. It is advisable
 *  to specify "f" is you do not want the program to stop and prompt for "Yes" when trying
 *  to delete write protected files.
 *  @return string Stdout of the "rm" command. En emty string == all deleted fine. 
 */
function soDeleteFiles($Mask, $Options = "") {
    if ($Options && (substr($Options, 0, 1) != "-")) $Options = "-".$Options;
    return soRunCommand("rm ".$Options." ".$Mask);
}; // soDeleteFiles


/** Backup routine to copy files from folder to another.
 *  This is a simple analyser and copier routine.
 *  It creates folders on the target folder called "yyyymmdd-hhmm" and copies recursively
 *  the source folder to it.
 *  @param string $Source Source folder name to back up.
 *  @param string $Target Target folder to back up to.
 *  @param int $Verbose Verbosity level 0=quiet, 1=more,2=a lotof info
 */
function soBackupFolder($Source, $Target, $Verbose = 0) {

    $target_device = soGetDeviceName($Target);
    $target_folder = $Target."/".date("Ymd-Hi", time());

    if ($Verbose) echo "Calculating used space by '$Source'\n";
    $source_folder_size = soGetFolderSize($Source) *1.05;
    $dir = new soDir($Target, "dir", "n");
    $icount = $dir->count();
    $i = 2;

    while ((soGetDeviceSpace($target_device, "a") < $source_folder_size) &&
           ($i++ < $icount)) {
               if ($Verbose) {
               echo "Deleting: ".$result.$Target."/".
                    $dir->getValue($i, "n")."\n";
           }; // if
           soDeleteFiles($Target."/".$dir->getValue($i, "n"), "Rf");
    }; // while

    ($Verbose > 1) ? ($param = "v") : ($param = "q");
    soRunCommand("cd $Source; mkdir $target_folder; ".
                 "rsync -".$param."a . $target_folder 2>/dev/null");
}; // soBackupFolder


#echo soBackupFolder("/data", "/dos")."\n";
#echo soRunCommand('find /kolmas -type f', 5)."\n";

?>
